# -*- coding: utf-8 -*-
"""
:mod:`tgwsdoc` Sphinx extension
-------------------------------

Automatically generate documentation for TGWebService controllers and
methods in the autodoc fashion with added examples.

Installing the extension
~~~~~~~~~~~~~~~~~~~~~~~~

#. In your conf.py file, add "tgwsdoc" in the extensions list. If needed
   add the directory containing tgwsdoc.py to the path.

#. Copy "toggle.js" and "toggle.css" into the "_static" directory.

Added directives
~~~~~~~~~~~~~~~~

.. directive:: .. autotgwstype:: myapp.controllers.apitypes.MyComplexType

    Generate a nice documentation for complex types. It includes a classic
    documentation of class attributes, plus two example of serialisation :
    one XML and one json.

    If the complex type has no default constuctor (ie taking no parameter)
    or if one want meaningfull datas to be shown, define a "sample" method
    on the class. If existing, it will be used as a constructor by tgwsdoc
    to instanciate the class.

.. directive:: .. tgwsrootcontroller:: myapp.controllers.RootWebServiceController /<url>

    Define, for later use by the other directives, where is the web
    services root. The "/<url>" is the "mount point" of your web service
    root controller. For example, "/api".

.. directive:: .. autotgwscontroller:: myapp.controllers.AWebServiceController

    Insert the docstring documentation of the controller. If a
    tgwsrootcontroller directive is present earlier in the rst file, the
    actual url of the controller (which is the base path of its
    methods for REST+[JSON|XML]) is calculated and inserted.

    As with autoclass (from the autodoc extension), the ``members`` option
    will also autodocument members, but only the exposed methods will be
    included.

.. directive:: .. autotgwsfunction:: myapp.controller.AWebServiceController.anexposedmethod
    
    Insert the docstring documentation of the function.

    In addition, it computes an adapted signature for the function, and
    documents the parameters with links to the types definition if they
    exists.

"""

from sphinx.ext import autodoc
from sphinx.domains.python import py_sig_re
from docutils.statemachine import ViewList

from tgwebservices.controllers import WebServicesRoot, WebServicesController
from tgwebservices import runtime

import simplejson
from turbojson import jsonify
from tgwebservices.xml_ import xml_value

from xml.etree import cElementTree as ElementTree
import datetime

def pseudo_ws_function():
    pass
fi = runtime.register(pseudo_ws_function)

def xml_indent(elem, level=0):
    i = "\n" + level*"  "
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + "  "
        for e in elem:
            xml_indent(e, level+1)
        if not e.tail or not e.tail.strip():
            e.tail = i
    if level and (not elem.tail or not elem.tail.strip()):
        elem.tail = i

def get_type_name(checkitem, withref=True, compact=False):
    key_type = u''
    if isinstance(checkitem, list):
        fmt = compact and u"[%s]" or _(u"list of %s")
        return fmt % get_type_name(checkitem[0], withref, compact)
    if isinstance(checkitem, runtime.typedproperty):
        checkitem = checkitem.type
    if not isinstance(checkitem, type):
         checkitem = checkitem.__class__
    if checkitem == runtime.typedproperty:
        key_type += checkitem.type
    elif checkitem in runtime.primitives:
        if checkitem is str:
            key_type += 'string'
        elif checkitem is int:
            key_type += 'integer'
        else:
            key_type += checkitem.__name__
    else:
        if withref:
            key_type += ':class:`%s`' % checkitem.__name__
        else:
            key_type += checkitem.__name__
    return key_type
    
def find_path(root, leaf):
    for key in dir(root):
        attr = getattr(root, key)
        if attr.__class__ is leaf:
            return [key]
            
        if isinstance(attr, WebServicesController):
            path = find_path(attr, leaf)
            if path is not None:
                return [key] + path

def prepare_json_strings(data, indent=0):
    json = jsonify.encode(data)
    json = simplejson.dumps(
            simplejson.loads(json), sort_keys=True, indent=4)

    strings = []
    for line in json.split('\n'):
        strings.append(u"%s%s" % (' '*indent, line))
    strings.append(u'')
    return strings

def prepare_xml_strings(tag, value, indent=0):
    xml = ElementTree.fromstring(str(xml_value(tag, value)))
    xml_indent(xml)
    strings = []
    for line in ElementTree.tostring(xml).split('\n'):
        strings.append(u'%s%s' % (' '*indent, line))
    strings.append(u'')
    return strings

def make_sample_value(objtype):
    obj = objtype
    if isinstance(obj, list):
        obj = obj[0]
    
    if hasattr(obj, 'sample'):
        obj = obj.sample()
    elif obj is basestring:
        obj = ''
    elif obj is datetime.date:
        obj = obj(2010, 04, 23)
    elif obj is datetime.time:
        obj = obj(12, 32, 45)
    elif obj is datetime.datetime:
        obj = obj(2010, 04, 23, 12, 32, 45)
    else:
        obj = obj()

    if isinstance(objtype, list):
        obj = [obj]

    return obj

class RootControllerDocumenter(autodoc.ClassDocumenter):
    objtype = "tgwsrootcontroller"
    directivetype = "class"

    @classmethod
    def can_document_member(cls, member, membername, isattr, parent):
        return isinstance(member, WebServicesRoot)

class ControllerDocumenter(autodoc.ClassDocumenter):
    objtype = "tgwscontroller"
    directivetype = "class"

    def getcontrollerurl(self, controller):
        root = self.env.tgws_rootcontroller
        path = find_path(root, controller)
        return (path is not None and '/'.join(path) or '')

    @classmethod
    def can_document_member(cls, member, membername, isattr, parent):
        return isinstance(member, WebServicesController)

    def filter_members(self, members, want_all):
        ret = super(ControllerDocumenter, self).filter_members(members, want_all)
        ret = [m for m in ret if hasattr(m[1], "_ws_func_info")]
        return ret

    def add_directive_header(self, sig):
        """Add the directive header and options to the generated content."""
        if self.object == self.env.tgws_rootcontroller:
            self.add_line(u'Root Namespace', '<tgwsdoc>')
        else:
            self.add_line(u'Namespace **%s**' % self.getcontrollerurl(self.object), '<tgwsdoc>')
        self.add_line(u'', '<tgwsdoc>')
        self.add_line(u'   Path', '<tgwsdoc>')
        self.add_line(u'       %s%s' % (
            self.env.tgws_baseurl, self.getcontrollerurl(self.object)), '<tgwsdoc>')

class ControllerMethodDocumenter(autodoc.MethodDocumenter):
    objtype = 'tgwsfunction'
    directivetype = 'method'
    priority = 1

    @classmethod
    def can_document_member(cls, member, membername, isattr, parent):
        return isinstance(parent, ControllerDocumenter) and hasattr(member, "_ws_func_info")

    def import_object(self):
        if super(ControllerMethodDocumenter, self).import_object():
            self.func_fi = runtime.register(self.object)
            return True
        else:
            return False

    def format_signature(self):
#        if len(self.func_fi._params) == 0:
#            return ""
        mandatory_params = [p for p in self.func_fi._params if p not in self.func_fi.optional]
        sig = ''
        sig += '(' + ', '.join(mandatory_params)
        if len(self.func_fi.optional) > 0:

            sig += '[,' + '[, '.join(self.func_fi.optional) \
                + ']'*len(self.func_fi.optional)
        sig += ')'
        if self.func_fi.return_type is not basestring:
            sig += "-> %s" % get_type_name(self.func_fi.return_type, False, True)
        return sig

    def add_directive_header(self, sig):
        """Add the directive header and options to the generated content."""
        directive = getattr(self, 'directivetype', self.objtype)
        name_in_directive = self.objpath[-1]
        self.add_line(u'.. %s:: %s%s' % (directive, name_in_directive, sig),
                      '<autodoc>')
        if self.options.noindex:
            self.add_line(u'   :noindex:', '<autodoc>')

    def get_doc(self, encoding=None):
        docstrings = super(autodoc.MethodDocumenter, self).get_doc(encoding)
        docstrings = [line for line in self.process_doc(docstrings)]

        samples = []

        sample_params = {}
        for p in self.func_fi._params:
            sample_params[p] = make_sample_value(
                self.func_fi.input_types[p])

        robj = make_sample_value(self.func_fi.return_type)

        samples.extend([
            u'.. cssclass:: toggle',
            u'',
            u'JSON params',
            u'    .. code-block:: javascript',
            u'',
        ])
        samples.extend(prepare_json_strings(sample_params, 8))

        samples.extend([
            u'JSON result',
            u'    .. code-block:: javascript',
            u'',
        ])

        samples.extend(prepare_json_strings(
            {'result': robj}, 8))

        samples.extend([
            u'XML params',
            u'    .. code-block:: xml',
            u'',
        ])
        samples.extend(prepare_xml_strings('parameters', 
            sample_params, 8))

        samples.extend([
            u'XML result',
            u'    .. code-block:: xml',
            u'',
        ])
        samples.extend(prepare_xml_strings('result', robj, 8))

        samples = [u'.. only:: html', u'',
            ] + [u'    '+line for line in samples]

        docstrings = samples + docstrings

        for p in self.func_fi._params:
            param_index = None
            for i, line in enumerate(docstrings):
                if line.strip().startswith(u':param %s:' % p):
                    param_index = i
                    break
            if param_index is None:
                docstrings.append(u':param %s:' % p)
                param_index = len(docstrings)-1

            docstrings.insert(param_index, u':type %s: %s' % (
                p, get_type_name(self.func_fi.input_types[p])))

        if self.func_fi.return_type is not basestring:
            docstrings.append(u':rtype: %s' %
                get_type_name(self.func_fi.return_type))

        return [docstrings]

    def add_content_(self, more_content, no_docstring=False):
        mycontent = []
        for p in self.func_fi._params:
            mycontent.append(u':param %s:' % ( p, ))
            mycontent.append(u':type %s: %s' % (
                p, get_type_name(self.func_fi.input_types[p])))
        if self.func_fi.return_type is not basestring:
            mycontent.append(u':rtype: %s' %
                get_type_name(self.func_fi.return_type))
        content = ViewList(mycontent, source='<tgws>')
        if more_content is not None:
            content += more_content
        super(ControllerMethodDocumenter, self).add_content(
            content, no_docstring)


class TypeAttributeDocumenter(autodoc.ClassDocumenter):
    objtype = 'tgwstypeattr'
    priority = 10

    @classmethod
    def can_document_member(cls, member, membername, isattr, parent):
        return isinstance(parent, TypeDocumenter)

    def get_doc(self, encoding=None):
        return []

    def add_content(self, more_content, no_docstring=False):
        content = ViewList(
            [_('type: %s') % get_type_name(self.object, True)], source='')
        autodoc.ModuleLevelDocumenter.add_content(
            self, content, no_docstring=no_docstring)
    
class TypeDocumenter(autodoc.ClassDocumenter):
    objtype = 'tgwstype'
    directivetype = 'class'

    @classmethod
    def can_document_member(cls, member, membername, isattr, parent):
        # we don't want to be automaticaly used
        return False

    def add_directive_header(self, sig):
        """Add the directive header and options to the generated content."""
        directive = getattr(self, 'directivetype', self.objtype)
        name_in_directive = self.objpath[-1]
        self.add_line(u'.. %s:: %s%s' % (directive, name_in_directive, sig),
                      '<autodoc>')
        if self.options.noindex:
            self.add_line(u'   :noindex:', '<autodoc>')

    def format_signature(self):
        return ""

    def import_object(self):
        if super(TypeDocumenter, self).import_object():
            runtime.register_complex_type(fi, self.object)
            return True
        else:
            return False

    def filter_members(self, members, want_all):
        ret = super(TypeDocumenter, self).filter_members(members, want_all)
        keys = runtime.ctvalues(self.object)
        return [m for m in ret if m[0] in keys]

    def add_content(self, more_content, no_docstring=False):
        sample_value = make_sample_value(self.object)

        samples_content = [
            u'.. cssclass:: toggle',
            u'',
            _(u'JSON Sample'),
            u'    .. code-block:: javascript',
            u'']

        samples_content.extend(
            prepare_json_strings(sample_value, 8))

        samples_content.append(_(u'XML sample'))
        samples_content.append(u'    .. code-block:: xml')
        samples_content.append(u'')

        samples_content.extend(prepare_xml_strings(
            self.object.__name__.lower(), sample_value, 8))

        samples_content = [u'.. only:: html', u'',
            ] + [u'    '+unicode(line) for line in samples_content]

        more_content = more_content + ViewList(samples_content, source='<tgwsdoc>')
        super(TypeDocumenter, self).add_content(more_content, no_docstring=no_docstring)

        return

    def __init__(self, directive, name, indent=u''):
        super(TypeDocumenter, self).__init__(directive, name, indent)
        self.options.members = autodoc.ALL
        self.options.inherited_members = True
        self.options.undoc_members = True

def tgwsrootcontroller_directive(dirname, arguments, options, content, lineno,
                    content_offset, block_text, state, state_machine):

    env = state.document.settings.env
    path, base, args, retann = py_sig_re.match(arguments[0]).groups()

    path = path.rstrip('.')
    mod = __import__(path)
    components = path.split('.')
    for comp in components[1:]:
        mod = getattr(mod, comp)

    env.tgws_rootcontroller = getattr(mod, base)
    if len(arguments)>1:
        env.tgws_baseurl = arguments[1]
        if not env.tgws_baseurl.endswith('/'):
            env.tgws_baseurl += '/'
    else:
        env.tgws_baseurl = '/'
    return []

def setup(app):
    autodoc.add_documenter(TypeAttributeDocumenter)
    autodoc.add_documenter(ControllerMethodDocumenter)
    app.add_autodocumenter(RootControllerDocumenter)
    app.add_autodocumenter(ControllerDocumenter)
    app.add_autodocumenter(TypeDocumenter)
    app.add_autodocumenter(TypeAttributeDocumenter)

    app.add_directive('tgwsrootcontroller', tgwsrootcontroller_directive,
                    content = 0, arguments = (1, 1, 0))

    app.add_javascript('toggle.js')
    app.add_stylesheet('toggle.css')

